package proposallineage_test

import (
	"fmt"
	"strings"
	"testing"

	"github.com/shoenig/test/must"

	"github.com/git-town/git-town/v22/internal/config/configdomain"
	"github.com/git-town/git-town/v22/internal/forge/forgedomain"
	"github.com/git-town/git-town/v22/internal/git/gitdomain"
	"github.com/git-town/git-town/v22/internal/proposallineage"
	. "github.com/git-town/git-town/v22/pkg/prelude"
)

const noProposalForBranch = "no_proposal"

type mockProposalFinder struct{}

func (self *mockProposalFinder) FindProposal(branch, target gitdomain.LocalBranchName) (Option[forgedomain.Proposal], error) {
	if strings.Contains(branch.String(), noProposalForBranch) {
		return None[forgedomain.Proposal](), nil
	}
	var prNumber int
	for _, char := range branch {
		prNumber += int(char)
	}
	return Some(forgedomain.Proposal{
		Data: forgedomain.ProposalData{
			Body:         None[gitdomain.ProposalBody](),
			MergeWithAPI: false,
			Number:       1,
			Source:       branch,
			Target:       target,
			Title:        "Test Mocker",
			URL:          fmt.Sprintf("https://www.github.com/git-town/pull/%d", prNumber),
		},
		ForgeType: forgedomain.ForgeTypeForgejo,
	}), nil
}

func TestProposalStackLineageBuilder_CheckLineageAndProposals(t *testing.T) {
	t.Parallel()
	// arrange
	mainBranch := gitdomain.NewLocalBranchName("main")
	featureBranchA := gitdomain.NewLocalBranchName("a")
	featureBranchB := gitdomain.NewLocalBranchName("b")
	lineage := configdomain.NewLineageWith(configdomain.LineageData{
		featureBranchA: mainBranch,
		featureBranchB: featureBranchA,
	})
	var connector forgedomain.ProposalFinder = &mockProposalFinder{}
	args := proposallineage.ProposalStackLineageArgs{
		Connector:                Some(connector),
		CurrentBranch:            featureBranchA,
		Lineage:                  lineage,
		MainAndPerennialBranches: gitdomain.LocalBranchNames{mainBranch},
	}
	tree, err := proposallineage.NewTree(args)
	must.NoError(t, err)
	must.True(t, tree.BranchToProposal[mainBranch].IsNone())
	must.True(t, tree.BranchToProposal[featureBranchB].IsSome())
	must.True(t, tree.BranchToProposal[featureBranchA].IsSome())
	want := `

-------------------------
 - main
   - https://www.github.com/git-town/pull/97 :point_left:
     - https://www.github.com/git-town/pull/98

<sup>[Stack](https://www.git-town.com/how-to/github-actions-breadcrumb.html) generated by [Git Town](https://github.com/git-town/git-town)</sup>
`[1:]

	// act
	have := proposallineage.NewBuilder(args, MutableSome(tree))

	// assert
	builder, hasBuilder := have.Get()
	must.True(t, hasBuilder)
	actual := builder.Build(args)
	must.EqOp(t, want, actual)
}

func TestProposalStackLineageBuilder_CurrentBranchHasMultipleDirectDescendants(t *testing.T) {
	t.Parallel()
	// arrange
	/*
				git-town branch:
					main
							a
									b
											c
									d
							e
									f
		There is a stack: main -> a -> [b -> c, d]
		There is a separate stack: main -> e -> f
	*/
	mainBranch := gitdomain.NewLocalBranchName("main")
	featureBranchA := gitdomain.NewLocalBranchName("a")
	featureBranchB := gitdomain.NewLocalBranchName("b")
	featureBranchC := gitdomain.NewLocalBranchName("c")
	featureBranchD := gitdomain.NewLocalBranchName("d")
	featureBranchE := gitdomain.NewLocalBranchName("e")
	featureBranchF := gitdomain.NewLocalBranchName("f")
	lineage := configdomain.NewLineageWith(configdomain.LineageData{
		// first stack
		featureBranchA: mainBranch,
		featureBranchB: featureBranchA,
		featureBranchC: featureBranchB,
		featureBranchD: featureBranchA,
		// second stack
		featureBranchE: mainBranch,
		featureBranchF: featureBranchE,
	})

	t.Run("Stack Proposal For Branch A", func(t *testing.T) {
		t.Parallel()
		var connector forgedomain.ProposalFinder = &mockProposalFinder{}
		args := proposallineage.ProposalStackLineageArgs{
			Connector:                Some(connector),
			CurrentBranch:            featureBranchA,
			Lineage:                  lineage,
			MainAndPerennialBranches: gitdomain.LocalBranchNames{mainBranch},
		}
		want := `
-------------------------
 - main
   - https://www.github.com/git-town/pull/97 :point_left:
     - https://www.github.com/git-town/pull/98
       - https://www.github.com/git-town/pull/99
     - https://www.github.com/git-town/pull/100

<sup>[Stack](https://www.git-town.com/how-to/github-actions-breadcrumb.html) generated by [Git Town](https://github.com/git-town/git-town)</sup>
`
		have := proposallineage.NewBuilder(args, MutableNone[proposallineage.Tree]())
		builder, hasBuilder := have.Get()
		must.True(t, hasBuilder)
		// actual
		actual := builder.Build(args)
		// assert
		must.EqOp(t, want, actual)
	})

	t.Run("Stack Proposal For Branch B", func(t *testing.T) {
		t.Parallel()
		var connector forgedomain.ProposalFinder = &mockProposalFinder{}
		args := proposallineage.ProposalStackLineageArgs{
			Connector:                Some(connector),
			CurrentBranch:            featureBranchB,
			Lineage:                  lineage,
			MainAndPerennialBranches: gitdomain.LocalBranchNames{mainBranch},
		}
		want := `
-------------------------
 - main
   - https://www.github.com/git-town/pull/97
     - https://www.github.com/git-town/pull/98 :point_left:
       - https://www.github.com/git-town/pull/99
     - https://www.github.com/git-town/pull/100

<sup>[Stack](https://www.git-town.com/how-to/github-actions-breadcrumb.html) generated by [Git Town](https://github.com/git-town/git-town)</sup>
`
		have := proposallineage.NewBuilder(args, MutableNone[proposallineage.Tree]())
		builder, hasBuilder := have.Get()
		must.True(t, hasBuilder)
		// actual
		actual := builder.Build(args)
		// assert
		must.EqOp(t, want, actual)
	})

	t.Run("Stack Proposal For Branch C", func(t *testing.T) {
		t.Parallel()
		var connector forgedomain.ProposalFinder = &mockProposalFinder{}
		args := proposallineage.ProposalStackLineageArgs{
			Connector:                Some(connector),
			CurrentBranch:            featureBranchC,
			Lineage:                  lineage,
			MainAndPerennialBranches: gitdomain.LocalBranchNames{mainBranch},
		}
		want := `
-------------------------
 - main
   - https://www.github.com/git-town/pull/97
     - https://www.github.com/git-town/pull/98
       - https://www.github.com/git-town/pull/99 :point_left:
     - https://www.github.com/git-town/pull/100

<sup>[Stack](https://www.git-town.com/how-to/github-actions-breadcrumb.html) generated by [Git Town](https://github.com/git-town/git-town)</sup>
`
		have := proposallineage.NewBuilder(args, MutableNone[proposallineage.Tree]())
		builder, hasBuilder := have.Get()
		must.True(t, hasBuilder)
		// actual
		actual := builder.Build(args)
		// assert
		must.EqOp(t, want, actual)
	})

	t.Run("Stack Proposal For Branch D", func(t *testing.T) {
		t.Parallel()
		var connector forgedomain.ProposalFinder = &mockProposalFinder{}
		args := proposallineage.ProposalStackLineageArgs{
			Connector:                Some(connector),
			CurrentBranch:            featureBranchD,
			Lineage:                  lineage,
			MainAndPerennialBranches: gitdomain.LocalBranchNames{mainBranch},
		}
		want := `
-------------------------
 - main
   - https://www.github.com/git-town/pull/97
     - https://www.github.com/git-town/pull/100 :point_left:
     - https://www.github.com/git-town/pull/98
       - https://www.github.com/git-town/pull/99

<sup>[Stack](https://www.git-town.com/how-to/github-actions-breadcrumb.html) generated by [Git Town](https://github.com/git-town/git-town)</sup>
`
		have := proposallineage.NewBuilder(args, MutableNone[proposallineage.Tree]())
		builder, hasBuilder := have.Get()
		must.True(t, hasBuilder)
		// actual
		actual := builder.Build(args)
		// assert
		must.EqOp(t, want, actual)
	})

	t.Run("Stack Proposal For Branch E", func(t *testing.T) {
		t.Parallel()
		var connector forgedomain.ProposalFinder = &mockProposalFinder{}
		args := proposallineage.ProposalStackLineageArgs{
			Connector:                Some(connector),
			CurrentBranch:            featureBranchE,
			Lineage:                  lineage,
			MainAndPerennialBranches: gitdomain.LocalBranchNames{mainBranch},
		}
		want := `
-------------------------
 - main
   - https://www.github.com/git-town/pull/101 :point_left:
     - https://www.github.com/git-town/pull/102

<sup>[Stack](https://www.git-town.com/how-to/github-actions-breadcrumb.html) generated by [Git Town](https://github.com/git-town/git-town)</sup>
`
		have := proposallineage.NewBuilder(args, MutableNone[proposallineage.Tree]())
		builder, hasBuilder := have.Get()
		must.True(t, hasBuilder)
		// actual
		actual := builder.Build(args)
		// assert
		must.EqOp(t, want, actual)
	})

	t.Run("Stack Proposal For Branch F", func(t *testing.T) {
		t.Parallel()
		var connector forgedomain.ProposalFinder = &mockProposalFinder{}
		args := proposallineage.ProposalStackLineageArgs{
			Connector:                Some(connector),
			CurrentBranch:            featureBranchF,
			Lineage:                  lineage,
			MainAndPerennialBranches: gitdomain.LocalBranchNames{mainBranch},
		}
		want := `
-------------------------
 - main
   - https://www.github.com/git-town/pull/101
     - https://www.github.com/git-town/pull/102 :point_left:

<sup>[Stack](https://www.git-town.com/how-to/github-actions-breadcrumb.html) generated by [Git Town](https://github.com/git-town/git-town)</sup>
`
		have := proposallineage.NewBuilder(args, MutableNone[proposallineage.Tree]())
		builder, hasBuilder := have.Get()
		must.True(t, hasBuilder)
		// actual
		actual := builder.Build(args)
		// assert
		must.EqOp(t, want, actual)
	})
}

func TestProposalStackLineageBuilder_NoLineage(t *testing.T) {
	t.Parallel()
	// arrange
	lineage := configdomain.NewLineageWith(configdomain.LineageData{})
	var connector forgedomain.ProposalFinder = &mockProposalFinder{}
	args := proposallineage.ProposalStackLineageArgs{
		Connector:                Some(connector),
		CurrentBranch:            gitdomain.NewLocalBranchName("main"),
		Lineage:                  lineage,
		MainAndPerennialBranches: lineage.Roots(),
	}
	expected := None[proposallineage.Builder]()

	// act
	actual := proposallineage.NewBuilder(args, MutableNone[proposallineage.Tree]())

	// assert
	must.EqOp(t, expected, actual)
}

func TestProposalStackLineageBuilder_NoLineageForMainAndPerennialBranches(t *testing.T) {
	t.Parallel()
	// arrange
	mainBranch := gitdomain.NewLocalBranchName("main")
	featureBranchA := gitdomain.NewLocalBranchName("a")
	lineage := configdomain.NewLineageWith(configdomain.LineageData{
		featureBranchA: mainBranch,
	})
	var connector forgedomain.ProposalFinder = &mockProposalFinder{}
	args := proposallineage.ProposalStackLineageArgs{
		Connector:                Some(connector),
		CurrentBranch:            mainBranch,
		Lineage:                  lineage,
		MainAndPerennialBranches: lineage.Roots(),
	}
	expected := None[proposallineage.Builder]()

	// act
	actual := proposallineage.NewBuilder(args, MutableNone[proposallineage.Tree]())

	// assert
	must.EqOp(t, expected, actual)
}

func TestProposalStackLineageBuilder_PrependBranch(t *testing.T) {
	t.Parallel()
	/*
		In this test we will simulate what occurs when a prepend operation is executed.

		Say that you have the current stack

		$ git-town branch

		main
			branch-a
				branch-b
			*		branch-d

		----
		Each of the non-perennial branches have proposals but when you run (for example)

		$ git-town prepend branch-c
		$ git-town branch

		main
			branch-a
				branch-b
			*		branch-c
						branch-d

		---

		Because branch-c is a new branch, it will not have a proposal but will be a target
		of proposal where the base=branch-d and the target=branch-c


		Even though there is branch with a missing proposal in the middle of the stack,
		we should try to show all the available branches leading up to the perennial branch.
	*/

	// arrange
	/*
		Original State
	*/
	mainBranch := gitdomain.NewLocalBranchName("main")
	featureBranchA := gitdomain.NewLocalBranchName("a")
	featureBranchB := gitdomain.NewLocalBranchName("b")
	featureBranchD := gitdomain.NewLocalBranchName("d")
	lineage := configdomain.NewLineageWith(configdomain.LineageData{
		featureBranchA: mainBranch,
		featureBranchB: featureBranchA,
		featureBranchD: featureBranchB,
	})
	var connector forgedomain.ProposalFinder = &mockProposalFinder{}
	args := proposallineage.ProposalStackLineageArgs{
		Connector:                Some(connector),
		CurrentBranch:            featureBranchA,
		Lineage:                  lineage,
		MainAndPerennialBranches: gitdomain.LocalBranchNames{mainBranch},
	}

	tree, err := proposallineage.NewTree(args)
	must.NoError(t, err)
	builder, hasBuilder := proposallineage.NewBuilder(args, MutableSome(tree)).Get()
	must.True(t, hasBuilder)
	original := `

-------------------------
 - main
   - https://www.github.com/git-town/pull/97 :point_left:
     - https://www.github.com/git-town/pull/98
       - https://www.github.com/git-town/pull/100

<sup>[Stack](https://www.git-town.com/how-to/github-actions-breadcrumb.html) generated by [Git Town](https://github.com/git-town/git-town)</sup>
`[1:]
	must.EqOp(t, original, builder.Build(args))

	// act - branch prepended
	featureBranchC := gitdomain.NewLocalBranchName("c-" + noProposalForBranch)
	lineage = lineage.
		// child -> parent
		Set(featureBranchC, featureBranchB).
		// child -> parent
		Set(featureBranchD, featureBranchC)

	// assert
	must.True(t, lineage.Parent(featureBranchA).EqualSome(mainBranch))
	must.True(t, lineage.Parent(featureBranchB).EqualSome(featureBranchA))
	must.True(t, lineage.Parent(featureBranchC).EqualSome(featureBranchB))
	must.True(t, lineage.Parent(featureBranchD).EqualSome(featureBranchC))

	args = proposallineage.ProposalStackLineageArgs{
		Connector:                Some(connector),
		CurrentBranch:            featureBranchA,
		Lineage:                  lineage,
		MainAndPerennialBranches: gitdomain.LocalBranchNames{mainBranch},
	}
	tree, err = proposallineage.NewTree(args)
	must.NoError(t, err)
	builder, hasBuilder = proposallineage.NewBuilder(args, MutableSome(tree)).Get()
	must.True(t, hasBuilder)
	must.EqOp(t, original, builder.Build(args))
}

func TestProposalStackLineageBuilder_SwapBranch(t *testing.T) {
	t.Parallel()
	// arrange
	mainBranch := gitdomain.NewLocalBranchName("main")
	featureBranchA := gitdomain.NewLocalBranchName("a")
	featureBranchB := gitdomain.NewLocalBranchName("b")
	lineage := configdomain.NewLineageWith(configdomain.LineageData{
		featureBranchA: mainBranch,
		featureBranchB: featureBranchA,
	})
	var connector forgedomain.ProposalFinder = &mockProposalFinder{}
	args := proposallineage.ProposalStackLineageArgs{
		Connector:                Some(connector),
		CurrentBranch:            featureBranchA,
		Lineage:                  lineage,
		MainAndPerennialBranches: gitdomain.LocalBranchNames{mainBranch},
	}

	tree, err := proposallineage.NewTree(args)
	must.NoError(t, err)
	builder, hasBuilder := proposallineage.NewBuilder(args, MutableSome(tree)).Get()
	must.True(t, hasBuilder)
	original := `

-------------------------
 - main
   - https://www.github.com/git-town/pull/97 :point_left:
     - https://www.github.com/git-town/pull/98

<sup>[Stack](https://www.git-town.com/how-to/github-actions-breadcrumb.html) generated by [Git Town](https://github.com/git-town/git-town)</sup>
`[1:]
	must.EqOp(t, original, builder.Build(args))

	/*
			*  - update lineage - swap
		  *  - update stack within the builder
	*/
	lineage.Set(featureBranchB, mainBranch)
	lineage.Set(featureBranchA, featureBranchB)
	args.Lineage = lineage
	must.NoError(t, builder.UpdateStack(args))
	want := `

-------------------------
 - main
   - https://www.github.com/git-town/pull/98
     - https://www.github.com/git-town/pull/97 :point_left:

<sup>[Stack](https://www.git-town.com/how-to/github-actions-breadcrumb.html) generated by [Git Town](https://github.com/git-town/git-town)</sup>
`[1:]

	// act
	actual := builder.Build(args)

	// assert
	must.EqOp(t, want, actual)
}
